#include "tv.h"

int main(int argc, char *argv[]) {
#ifdef WITH_TV_GRPAPHICSMAGICK
  Magick::InitializeMagick(*argv);
#endif

  bool verbose = false;
  const tv::TermSizeResult term = tv::DetermineTermSize();

  tv::DisplayOptions display_opts;
  tv::PresentationOptions present;
  present.terminal_use_upper_block = tv::GetBoolenEnv("TV_USE_UPPER_BLOCK");

  std::string bg_color = "auto";
  const char *bg_pattern_color = nullptr;
  display_opts.allow_frame_skipping = tv::GetBoolenEnv("TV_ALLOW_FRAME_SKIP");

  int output_fd = STDOUT_FILENO;
  std::vector<std::string> filelist; // from -f<filelist> and command line
  int frame_offset = 0;
  int max_frames = tv::kNotInitialized;
  bool do_img_loading = true;
  bool do_vid_loading = true;
  int thread_count = kDefaultThreadCount;
  int geometry_width = (term.cols - 2);
  int geometry_height = (term.rows - 2);
  bool debug_no_frame_delay = false;

  if (auto pixelation_from_env = ParsePixelation(getenv("TV_PIXELATION"));
      pixelation_from_env.has_value()) {
    present.pixelation = *pixelation_from_env;
  }

  // Convenience predicates: pixelation sending high-res images, no blocks.
  const auto is_pixel_direct_with_alpha = [](Pixelation p) {
    return p == Pixelation::kKittyGraphics || p == Pixelation::kiTerm2Graphics;
  };
  const auto is_pixel_direct_p = [](Pixelation p) {
    return p == Pixelation::kKittyGraphics || p == Pixelation::kiTerm2Graphics
#ifdef WITH_TV_SIXEL
           || p == Pixelation::kSixelGraphics
#endif
        ;
  };

  enum LongOptionIds {
    OPT_CLEAR_SCREEN = 1000,
    OPT_COLOR_256,
    OPT_COMPRESS_PIXEL,
    OPT_NO_FRAME_DELAY,
    OPT_FRAME_COUNT,
    OPT_FRAME_OFFSET,
    OPT_GRID,
    OPT_PATTERN_SIZE,
    OPT_ROTATE,
    OPT_THREADS,
    OPT_TITLE,
    OPT_VERBOSE,
    OPT_VERSION,
    OPT_MANPAGE_HELP,
    OPT_AUTO_CROP,
    OPT_SCROLL,
  };

  // Flags with optional parameters need to be long-options, as on MacOS,
  // there is no way to have single-character options with
  static constexpr struct option long_options[] = {
      {"auto-crop", optional_argument, NULL, OPT_AUTO_CROP},
      {"center", no_argument, NULL, 'C'},
      {"clear", optional_argument, NULL, OPT_CLEAR_SCREEN},
      {"color8", no_argument, NULL, OPT_COLOR_256},
      {"compress", optional_argument, NULL, OPT_COMPRESS_PIXEL},
      {"delta-move", required_argument, NULL, 'd'},
      {"debug-no-frame-delay", no_argument, NULL, OPT_NO_FRAME_DELAY},
      {"frame-offset", required_argument, NULL, OPT_FRAME_OFFSET},
      {"fit-width", no_argument, NULL, 'W'},
      {"frames", required_argument, NULL, OPT_FRAME_COUNT},
      {"grid", required_argument, NULL, OPT_GRID},
      {"help", no_argument, NULL, OPT_MANPAGE_HELP},
      {"loops", optional_argument, NULL, 'c'},
      {"pattern-size", required_argument, NULL, OPT_PATTERN_SIZE},
      {"pixelation", required_argument, NULL, 'p'},
      {"rotate", required_argument, NULL, OPT_ROTATE},
      {"scroll", optional_argument, NULL, OPT_SCROLL},
      {"threads", required_argument, NULL, OPT_THREADS},
      {"title", optional_argument, NULL, OPT_TITLE},
      {"upscale", optional_argument, NULL, 'U'},
      {"verbose", no_argument, NULL, OPT_VERBOSE},
      {"version", no_argument, NULL, OPT_VERSION},
      {0, 0, 0, 0}};

  int opt;
  int option_index = 0;
  while ((opt = getopt_long(argc, argv, "vg:w:t:c:f:b:B:hCF:Ed:UWaVIo:f:p:h",
                            long_options, &option_index)) != -1) {
    switch (opt) {
    case 'g':
      // Parse xHEIGHT, WIDTHx, WIDTHxHEIGHT
      if ((sscanf(optarg, "x%d", &geometry_height) == 0) &&
          (sscanf(optarg, "%dx%d", &geometry_width, &geometry_height) < 1)) {
        fprintf(stderr, "Invalid size spec '%s'", optarg);
        return usage(argv[0], ExitCode::kParameterError, geometry_width,
                     geometry_height);
      }
      break;
    case 'w':
      if (optarg[0] == 'r') {
        present.duration_for_row =
            Duration::Millis(roundf(atof(optarg + 1) * 1000.0f));
      } else {
        present.duration_between_images =
            Duration::Millis(roundf(atof(optarg) * 1000.0f));
      }
      break;
    case 't':
      present.duration_per_image =
          Duration::Millis(roundf(atof(optarg) * 1000.0f));
      if (present.duration_per_image.is_zero()) {
        fprintf(stderr, "Note, -t<zero-duration> will effectively "
                        "skip animations/movies\n");
      }
      break;
    case 'c': // Legacy option, now long opt. Keep for now.
      // No parameter --loop essentially defaults to loop forever.
      present.loops = optarg ? atoi(optarg) : -1;
      break;
    case OPT_CLEAR_SCREEN:
      if (optarg) {
        const int optlen = strlen(optarg);
        if (optlen <= 5 && strncasecmp(optarg, "every", optlen) == 0)
          present.clear_screen = ClearScreen::kBeforeEachImage;
        else {
          fprintf(stderr,
                  "Parameter for --clear can be 'every', "
                  "got %s\n",
                  optarg);
          return usage(argv[0], ExitCode::kParameterError, geometry_width,
                       geometry_height);
        }
      } else {
        present.clear_screen = ClearScreen::kBeforeFirstImage;
      }
      break;
    case OPT_FRAME_OFFSET:
      frame_offset = atoi(optarg);
      break;
    case OPT_FRAME_COUNT:
      max_frames = atoi(optarg);
      break;
    case 'a':
      display_opts.antialias = false;
      break;
    case 'b':
      bg_color = std::string(optarg);
      break;
    case 'B':
      bg_pattern_color = strdup(optarg);
      break;
    case OPT_PATTERN_SIZE:
      display_opts.pattern_size = atoi(optarg);
      break;
    case OPT_SCROLL:
      display_opts.scroll_animation = true;
      if (optarg != NULL) {
        display_opts.scroll_delay = Duration::Millis(atoi(optarg));
      }
      break;
    case 'V':
#ifdef WITH_TV_VIDEO
      do_img_loading = false;
      do_vid_loading = true;
#else
      fprintf(stderr, "-V: Video support not compiled in\n");
#endif
      break;
    case 'I':
      do_img_loading = true;
#if WITH_TV_VIDEO
      do_vid_loading = false;
#endif
      break;
    case OPT_ROTATE:
      // TODO(hzeller): Maybe later also pass angles ?
      if (strcasecmp(optarg, "exif") == 0) {
        display_opts.exif_rotate = true;
      } else if (strcasecmp(optarg, "off") == 0) {
        display_opts.exif_rotate = false;
      } else {
        fprintf(stderr, "--rotate=%s: expected 'exif' or 'off'\n", optarg);
        return usage(argv[0], ExitCode::kParameterError, geometry_width,
                     geometry_height);
      }
      break;
    case OPT_GRID:
      switch (sscanf(optarg, "%dx%d", &present.grid_cols, &present.grid_rows)) {
      case 0:
        fprintf(stderr, "Invalid grid spec '%s'", optarg);
        return usage(argv[0], ExitCode::kParameterError, geometry_width,
                     geometry_height);
      case 1:
        present.grid_rows = present.grid_cols;
        break;
      }
      break;
    case OPT_THREADS:
      thread_count = atoi(optarg);
      break;
    case 'd':
      if (sscanf(optarg, "%d:%d", &display_opts.scroll_dx,
                 &display_opts.scroll_dy) < 1) {
        fprintf(stderr,
                "--delta-move=%s: At least dx parameter needed"
                " e.g. --delta-move=1."
                "Or you can give dx, dy like so: -d1:-1",
                optarg);
        return usage(argv[0], ExitCode::kParameterError, geometry_width,
                     geometry_height);
      }
      break;
    case 'C':
      display_opts.center_horizontally = true;
      break;
    case 'U':
      display_opts.upscale = !display_opts.upscale;
      if (optarg) {
        if (optarg[0] == 'i' || optarg[0] == 'I') {
          display_opts.upscale_integer = true;
        } else {
          fprintf(stderr, "Invalid parameter to --upscale\n");
        }
      }
      break;
    case OPT_AUTO_CROP:
      display_opts.auto_crop = true;
      if (optarg) {
        display_opts.crop_border = atoi(optarg);
      }
      break;
    case 'F':
      if (!AppendToFileList(optarg, true, &filelist)) {
        return usage(argv[0], ExitCode::kFilelistProblem, geometry_width,
                     geometry_height);
      }
      break;
    case 'E':
      present.hide_cursor = false;
      break;
    case 'W':
      display_opts.fill_width = true;
      break;
    case OPT_VERSION:
      return PrintVersion(stdout);
    case OPT_TITLE:
      display_opts.show_title = !display_opts.show_title;
      if (optarg)
        display_opts.title_format = optarg;
      break;
    case 'o':
      output_fd = open(optarg, O_WRONLY | O_CREAT | O_TRUNC, 0664);
      if (output_fd < 0) {
        fprintf(stderr, "%s: %s\n", optarg, strerror(errno));
        return usage(argv[0], ExitCode::kCantOpenOutput, geometry_width,
                     geometry_height);
      }
      break;
    case 'f':
      if (!AppendToFileList(optarg, false, &filelist)) {
        return usage(argv[0], ExitCode::kFilelistProblem, geometry_width,
                     geometry_height);
      }
      break;
    case 'p':
      if (const auto p = ParsePixelation(optarg); p.has_value()) {
        present.pixelation = *p;
      } else {
        fprintf(stderr, "Unknown --pixelation/-p parameter '%s'\n", optarg);
      }
      break;
    case OPT_COMPRESS_PIXEL:
      if (optarg) {
        int level = atoi(optarg);
        level = (level >= 0 && level <= 9) ? level : 1;
        display_opts.compress_pixel_level = level;
      } else {
        display_opts.compress_pixel_level = 1;
      }
      break;
    case OPT_COLOR_256:
      present.use_256_color = true;
      break;
    case OPT_VERBOSE:
      verbose = true;
      break;
    case OPT_NO_FRAME_DELAY:
      debug_no_frame_delay = true;
      break;
    case OPT_MANPAGE_HELP:
      InvokeHelpPager();
      return 0;
      break;
    case 'h':
    default:
      return usage(
          argv[0],
          (opt == 'h' ? ExitCode::kSuccess : ExitCode::kParameterError),
          geometry_width, geometry_height);
    }
  }

  // -- A sieve of sanity checks and configuration refinement.

  if (geometry_width < 1 || geometry_height < 1) {
    if (term.cols < 0 || term.rows < 0) {
      fprintf(stderr, "Failed to read size from terminal; "
                      "Please supply -g<width>x<height> directly.\n");
    } else {
      fprintf(stderr, "%dx%d is a rather unusual size\n", geometry_width,
              geometry_height);
    }
    return usage(argv[0], ExitCode::kNotATerminal, geometry_width,
                 geometry_height);
  }

  bool cell_size_warning_needed = false;
  const bool cell_size_unknown_in_pixel_mode =
      ((term.font_width_px < 0 || term.font_height_px < 0) &&
       is_pixel_direct_p(present.pixelation));
  if (cell_size_unknown_in_pixel_mode) {
    // Best effort mode if someone requests graphics protocol, but
    // we don't know cell size in pixels.
    //
    // Either they want to create some output for another terminal, or
    // they are on a terminal that supports the graphics protocol but
    // not much else.
    //
    // For instance, Chromium Secure Shell (hterm) extension supports
    // iTerm2 graphics, but unfortunately does not report size :(
    // Also apparently termux (Issue #86)
    cell_size_warning_needed = (present.grid_cols > 1);
    max_frames = 1; // Since don't know how many cells move up next frame
    // We need a cell size to have something to scale the image into.
    display_opts.cell_x_px = 9; // Make up some typical values.
    display_opts.cell_y_px = 18;
    // hterm does _not_ support PNM, always convert to PNG.
    display_opts.compress_pixel_level = 1;
    // Because we don't know how much to move up and right. Also, hterm
    // does not seem to place an image in X-direction in the first place.
    present.grid_cols = 1;
  }

  // Determine best default to pixelate images.
  if (present.pixelation == Pixelation::kNotChosen) {
    present.pixelation = Pixelation::kQuarterBlock; // Good default.
    if (term.font_width_px > 0 && term.font_height_px > 0) {
      auto graphics_info = tv::QuerySupportedGraphicsProtocol();
      present.tmux_workaround = graphics_info.in_tmux;
      switch (graphics_info.preferred_graphics) {
      case tv::GraphicsProtocol::kIterm2:
        present.pixelation = Pixelation::kiTerm2Graphics;
        break;
      case tv::GraphicsProtocol::kKitty:
        present.pixelation = Pixelation::kKittyGraphics;
        break;
      case tv::GraphicsProtocol::kSixel:
#ifdef WITH_TV_SIXEL
        present.pixelation = Pixelation::kSixelGraphics;
        present.sixel_cursor_workaround =
            graphics_info.known_broken_sixel_cursor_placement;
#else
        present.pixelation = Pixelation::kQuarterBlock;
#endif
        break;
      case tv::GraphicsProtocol::kNone:
        break;
      }
    }
  } else if (present.pixelation == Pixelation::kKittyGraphics) {
    // If the user manually chooses kitty, we still need to know if in tmux
    auto graphics_info = tv::QuerySupportedGraphicsProtocol();
    present.tmux_workaround = graphics_info.in_tmux;
  }
#if defined(WITH_TV_SIXEL)
  // If the user manually choose sixel, we still can't avoid a terminal
  // query, as we have to figure out if it has a broken cursor implementation.
  else if (present.pixelation == Pixelation::kSixelGraphics) {
    auto graphics_info = tv::QuerySupportedGraphicsProtocol();
    present.sixel_cursor_workaround =
        graphics_info.known_broken_sixel_cursor_placement;
  }
#endif

  // The high-res image terminals provide alpha-blending, no need to
  // query the terminal color for 'auto'
  if (is_pixel_direct_with_alpha(present.pixelation) &&
      (strcasecmp(bg_color.c_str(), "auto") == 0)) {
    bg_color = "none";
  }

  // If 'none' is chosen for background color, that implies local alpha
  // handling; an optimization to emit RGB instead of RGBA for hi-res
  // terminals or sending whitespace for background cut-off in block-display.
  if ((strcasecmp(bg_color.c_str(), "none") == 0)) {
    display_opts.local_alpha_handling = false;
  }

  // If we're using block graphics, we might need to adapt the aspect ratio
  // slightly depending if the font-cell has a 1:2 ratio.
  // Terminals using direct pixels don't need this.
  const float stretch_correct =
      is_pixel_direct_p(present.pixelation)
          ? 1.0f
          : 0.5f * term.font_height_px / term.font_width_px;
  display_opts.width_stretch =
      tv::GetFloatEnv("TV_FONT_WIDTH_CORRECT", stretch_correct);

  switch (present.pixelation) {
  case Pixelation::kHalfBlock:
    display_opts.cell_x_px = 1;
    display_opts.cell_y_px = 2;
    break;
  case Pixelation::kQuarterBlock:
    display_opts.width_stretch *= 2;
    display_opts.cell_x_px = 2;
    display_opts.cell_y_px = 2;
    break;
#ifdef WITH_TV_SIXEL
  case Pixelation::kSixelGraphics:
#endif
  case Pixelation::kKittyGraphics:
  case Pixelation::kiTerm2Graphics:
    if (term.font_width_px > 0)
      display_opts.cell_x_px = term.font_width_px;
    if (term.font_height_px > 0)
      display_opts.cell_y_px = term.font_height_px;
    break;
  case Pixelation::kNotChosen:
    break; // Should not happen. Was set above.
  }
  display_opts.width = geometry_width * display_opts.cell_x_px;
  display_opts.height = geometry_height * display_opts.cell_y_px;

  for (int imgarg = optind; imgarg < argc && !interrupt_received; ++imgarg) {
    filelist.push_back(argv[imgarg]);
  }

  if (filelist.empty()) {
    fprintf(stderr, "Expected image filename(s) on command line "
                    "or via -f\n");
    return usage(argv[0], ExitCode::kImageReadError, geometry_width,
                 geometry_height);
  }

  // There is no scroll if there is no movement.
  if (display_opts.scroll_dx == 0 && display_opts.scroll_dy == 0) {
    fprintf(stderr, "Scrolling chosen, but dx:dy = 0:0. "
                    "Just showing image, no scroll.\n");
    display_opts.scroll_animation = false;
  }

  if (present.clear_screen == ClearScreen::kBeforeEachImage &&
      (present.grid_cols != 1 || present.grid_rows != 1)) {
    // Clear every only makes sense with no grid.
    present.clear_screen = ClearScreen::kBeforeFirstImage;
  }

  // If we scroll in one direction (so have 'infinite' space) we want fill
  // the available screen space fully in the other direction.
  display_opts.fill_width =
      display_opts.fill_width ||
      (display_opts.scroll_animation && display_opts.scroll_dy != 0);
  display_opts.fill_height = display_opts.scroll_animation &&
                             display_opts.scroll_dx != 0; // scroll h, fill v

  // Showing exactly one frame implies animation behaves as static image
  if (max_frames == 1) {
    present.loops = 1;
  }

  // If nothing is set to limit animations but we have multiple images,
  // set some sensible limit.
  if (filelist.size() > 1 && present.loops == tv::kNotInitialized &&
      present.duration_per_image == Duration::InfiniteFuture()) {
    present.loops = 1; // Don't get stuck on the first endless-loop
  }

  if (display_opts.show_title) {
    // Leave space for text.
    display_opts.height -= display_opts.cell_y_px * present.grid_rows;
  }

  // Asynconrous image loading (filelist.size()) and terminal query (+1)
  thread_count = (thread_count > 0 ? thread_count : kDefaultThreadCount);

  // Note: this thread pool will be leaked explicitly to not unnecessarily
  // have to wait on potentially blocking cleanup at program exit where it
  // does not matter.
  tv::ThreadPool *const pool =
      new tv::ThreadPool(std::min(thread_count, (int)filelist.size() + 1));

  std::future<rgba_t> background_color_future;
  if (strcasecmp(bg_color.c_str(), "auto") == 0) {
    std::function<rgba_t()> query_terminal = []() {
      return rgba_t::ParseColor(tv::QueryBackgroundColor());
    };
    // Finding the background color might take a while, so we query
    // it asynchonously and only force a wait on it once an image display
    // actually queries it.
    background_color_future = pool->ExecAsync(query_terminal);
    display_opts.bgcolor_getter = [&background_color_future]() {
      static rgba_t value = background_color_future.get(); // once
      return value;
    };
  } else {
    const rgba_t bg = rgba_t::ParseColor(bg_color.c_str());
    display_opts.bgcolor_getter = [bg]() { return bg; };
  }

  display_opts.bg_pattern_color = rgba_t::ParseColor(bg_pattern_color);

  // In a grid, we have less space per picture.
  display_opts.width /= present.grid_cols;
  display_opts.height /= present.grid_rows;

  ExitCode exit_code = ExitCode::kSuccess;

  std::mutex errors_lock; // Collect any errors to display later.
  std::deque<std::string> errors;

  // Async image loading, preparing them in a thread pool
  LoadedImageSources loaded_sources;
  for (const std::string &filename : filelist) {
    if (interrupt_received)
      break;
    std::function<tv::ImageSource *()> f =
        [filename, frame_offset, max_frames, do_img_loading, do_vid_loading,
         &display_opts, &exit_code, &errors_lock,
         &errors]() -> tv::ImageSource * {
      if (interrupt_received)
        return nullptr;
      // TODO: after switch to c++17, use variant in return ?
      std::string err;
      ImageSource *result =
          ImageSource::Create(filename, display_opts, frame_offset, max_frames,
                              do_img_loading, do_vid_loading, &err);
      if (!result) {
        std::unique_lock<std::mutex> l(errors_lock);
        exit_code = ExitCode::kImageReadError;
        if (!err.empty())
          errors.push_back(err);
      }
      return result;
    };
    loaded_sources.push_back(pool->ExecAsync(f));
  }

  // The aync write queue (BufferedWriteSequencer) lines up the next
  // buffers to be emitted.
  static constexpr int kAsyncWriteQueueSize = 4;

  // Since Unicode blocks emit differences, we can't skip frames in output.
  // TODO: should probably better ask the canvas directly instead.
  const bool buffer_allow_skipping = (display_opts.allow_frame_skipping &&
                                      is_pixel_direct_p(present.pixelation));
  tv::BufferedWriteSequencer sequencer(
      output_fd, buffer_allow_skipping, kAsyncWriteQueueSize,
      debug_no_frame_delay, interrupt_received);
  const Time start_show = Time::Now();
  const int successful_images =
      PresentImages(&loaded_sources, display_opts, present, &sequencer,
                    &cell_size_warning_needed);
  const Time end_show = Time::Now();

  // Error messages have been collected to not clutter the image output
  // when they happen. Emit them now.
  int max_errors_to_show = 4;
  for (const std::string &err : errors) {
    fprintf(stderr, "%s\n", err.c_str());
    if (--max_errors_to_show == 0)
      break;
  }
  if (!max_errors_to_show) {
    fprintf(stderr, "... total of %d errors\n", (int)errors.size());
  }

  if (interrupt_received) {
    // Even though we completed the write, some terminals sometimes seem
    // to get messed up, maybe interrupted escape sequence ?
    // Make sure to move to the very bottom and also reset attributes.
    // But do it on stderr, to not send it to a potentially redirected fd.
    fprintf(stderr, "\033[0m\033[%dB\n", term.rows);
    fflush(stderr);
  }

  if (verbose) {
    fprintf(stderr, "Terminal cells: %dx%d  cell-pixels: %dx%d\n", term.cols,
            term.rows, term.font_width_px, term.font_height_px);
    if (term.font_width_px < 0 || term.font_height_px < 0) {
      fprintf(stderr, "Note: Terminal does not return ws_xpixel and ws_ypixel "
                      "in TIOCGWINSZ ioctl or \"\\033[16t\" query.\n"
                      "\t->Aspect ratio might be off.\n"
                      "\t->File a feature request with the terminal "
                      "emulator program you use\n");
    }
    fprintf(stderr, "Active Geometry: %dx%d", geometry_width, geometry_height);
    if (is_pixel_direct_p(present.pixelation) && //
        term.font_width_px > 0 && term.font_height_px > 0) {
      fprintf(stderr, "; canvas-pixels: %dx%d",
              geometry_width * term.font_width_px,
              geometry_height * term.font_height_px);
    }
    fprintf(stderr, "\n");

    fprintf(stderr, "Effective pixelation: Using %s",
            PixelationToString(present.pixelation));
#ifdef WITH_TV_SIXEL
    if (present.pixelation == Pixelation::kSixelGraphics) {
      if (present.sixel_cursor_workaround) {
        fprintf(stderr, " (with cursor placment workaround)");
      } else {
        fprintf(stderr, " (with default cursor placement)");
      }
    }
#endif
    if (present.pixelation == Pixelation::kKittyGraphics) {
      if (present.tmux_workaround) {
        fprintf(stderr, " (with tmux workaround)");
      }
    }
    fprintf(stderr, ".\n");
    const rgba_t bg = display_opts.bgcolor_getter();
    fprintf(stderr, "Background color for transparency '%s'", bg_color.c_str());
    if (strcasecmp(bg_color.c_str(), "none") != 0) {
      fprintf(stderr, "; effective RGB #%02x%02x%02x", bg.r, bg.g, bg.b);
    }
    fprintf(stderr, "\n");
    const rgba_t checker_bg = display_opts.bg_pattern_color;
    if (checker_bg.a == 0xff) {
      fprintf(stderr,
              "\t-> Checker pattern color  '%s', "
              "RGB #%02x%02x%02x\n",
              bg_pattern_color, checker_bg.r, checker_bg.g, checker_bg.b);
    }
    if (display_opts.local_alpha_handling) {
      fprintf(stderr, "Alpha-channel merging with "
                      "background color done by tv.\n");
    } else {
      fprintf(stderr, "Alpha-channel handled by terminal.\n");
    }
    const Duration d = end_show - start_show;
    const uint64_t written_bytes =
        sequencer.bytes_total() - sequencer.bytes_skipped();
    fprintf(stderr,
            "%d file%s (%d successful); %s written (%s/s) "
            "%" PRId64 " frames",
            (int)filelist.size(), filelist.size() == 1 ? "" : "s",
            successful_images,
            tv::HumanReadableByteValue(written_bytes).c_str(),
            tv::HumanReadableByteValue(written_bytes / d).c_str(),
            sequencer.frames_total());
    // Only show FPS if we have one video or animation
    if (filelist.size() == 1 && sequencer.frames_total() > 50) {
      fprintf(stderr, "; %.1ffps", sequencer.frames_total() / d);
    }
    if (display_opts.allow_frame_skipping && sequencer.frames_total() > 0) {
      fprintf(stderr, " (%" PRId64 " skipped, %.1f%%)\n",
              sequencer.frames_skipped(),
              100.0 * sequencer.frames_skipped() / sequencer.frames_total());
    }
    fprintf(stderr, "\n");

    auto print_env = [](const char *env) {
      const char *value = getenv(env);
      fprintf(stderr, " %-23s%s", env, value ? " = " : "   (not set)\n");
      if (value)
        fprintf(stderr, "\"%s\"\n", value);
    };
    fprintf(stderr, "Environment variables\n");
    print_env("TV_PIXELATION");
    print_env("TV_DEFAULT_TITLE");
    print_env("TV_ALLOW_FRAME_SKIP");
    print_env("TV_USE_UPPER_BLOCK");
    print_env("TV_FONT_WIDTH_CORRECT");
  }

  if (cell_size_unknown_in_pixel_mode && cell_size_warning_needed) {
    fprintf(stderr,
            "Terminal does not support pixel size query, "
            "but with %s this is needed to show animations or columns.\n"
            "File an issue with your terminal implementation to implement "
            "ws_xpixel, ws_ypixel on TIOCGWINSZ or \"\\033[16t\" query.\n"
            "Can't show animations or have columns in grid.\n(Suggestion: "
            "switch back to --pixelation=quarter for now)\n",
            PixelationToString(present.pixelation));
  }

  // If we were super-fast decoding and showing images that didn't need
  // transparency, the query might still be running. Wait a tiny bit so
  // that the terminal does not spill the result on the screen once we've
  // returned. If the result is already there, this will return immediately
  if (background_color_future.valid())
    background_color_future.wait_for(std::chrono::milliseconds(200));

  free((void *)bg_pattern_color);

  // Deliberately leaking thread pool as we don't bother waiting for
  // lingering threads

  return (int)exit_code;
}
